package nethardware;

# Package for probing physical interface cards on a host.
# Should work on the following architectures:
# Solaris
# HP-UX
# Linux
# AIX
# FreeBSD

use Sys::Hostname;
use Carp;
use strict;
use Data::Dumper;

BEGIN {
	eval "use miitoollib;";
}

sub new {
my $self={};
my $class=shift;

	my $r=`uname`;
	chomp $r;
	$self->{OS}=$r;
	$self->{cards}={};
	$self->{have_miiquery}=0;
	$self->{have_sendarp}=0;
	if(defined(&miitoollib::miiquery)) {
		$self->{have_miiquery}=1;
	}
	if(defined(&miitoollib::send_arp)) {
		$self->{have_sendarp}=1;
	}

	# Each card will have the following details:

	# name -> IPv4	IPv4 address (if any)
	#	  HW	Hardware address HH:HH... format
	#	  NMv4	Network address for V4 IP
	#	  BCv4  Broadcast address for V4 IP
	#         IPv6	IPv6 address (if any)
	#	  NMv6	Bitcount for netmask (i.e 64)
	#	  SCv6	v6 Scope type, such as 'link'

	populate_cardinfo($self,$r);
	bless($self,$class);
	return $self;
}

sub populate_cardinfo {
my $self=shift;
my $os=shift;
my (@F,$fd,$cline);

	# For AIX we use "lsdev -C -c if -s EN" to get the en* devices, and 
	# then use ifconfig to get the details for each particular card.
	$self->{order}=[];
	my @ORD=();
	if($os eq "AIX") {
		open($fd,"/usr/sbin/lsdev -C -c if -s EN|") or confess("Can not read network hardware details via lsdev.");
		my %cards=();
		while($cline=<$fd>) {
			chomp $cline;
			@F=split(/\s+/,$cline);
			if($F[0] !~ /^en\d+$/) {
				next;
			}
			my $card=$F[0];
			push @ORD,$card;
			$cards{$card}=lc($F[1]);
			$self->{cards}->{$card}={};
			$self->{cards}->{$card}->{IPv4}="";
			$self->{cards}->{$card}->{NMv4}="";
			$self->{cards}->{$card}->{BCv4}="";
			$self->{cards}->{$card}->{IPv6}="";
			$self->{cards}->{$card}->{NMv6}="";
			$self->{cards}->{$card}->{HW}="";
			$self->{cards}->{$card}->{AGGR}=0; # link agg/failover
			$self->{cards}->{$card}->{AGGR_DETECT_RATE_HZ}=0; # non-zero if detectable
			$self->{cards}->{$card}->{AGGR_DESC}=""; # link agg/failover desc
		}
		close($fd);
		# %cards is list of cards available|defined
		my @delete=();
		my $txt=`/usr/bin/netstat -in 2>/dev/null`;
		my @L=split(/\n/,$txt);
		for my $cline (@L) {
			my @F2=split(/\s+/,$cline);
			next if @F2<4;
			if($F2[0] =~ /^en\d+/ && $F2[2] =~ /^link#/) {
				my $cc=$F2[0];
				$cc =~ s/\*//;
				my $h=$F2[3];
				my $hex="";
				my @F3=split(/\./,$h);
				$hex.=sprintf("%02x",hex($_)) foreach(@F3);
				$self->{cards}->{$cc}->{HW}=$hex;
			}
		}
		for my $card (keys %cards) {
			if($self->{cards}->{$card}->{HW} eq "") {
				push @delete,$card;
			}
		}
		delete $self->{cards}->{$_} foreach (@delete);
		delete $cards{$_} foreach (@delete);

		# Now use ifconfig to get the remaining detilas
		# for the cards on the system.

		open($fd,"/etc/ifconfig -a|") or confess("Can not load ifconfig details (/etc/ifconfig -a)");
		my $ccard;
		while($cline=<$fd>) {
			chomp $cline;
			if(substr($cline,0,1) =~ /\s/) {
				next if !defined($ccard);
				$cline =~ s/^\s+//;
				@F=split(/\s+/,$cline);
				my $cw=0;
				while($cw<@F-1) {
					if($F[$cw] eq "inet") {
						if($F[1+$cw] ne "0.0.0.0") {
							$self->{cards}->{$ccard}->{IPv4}=$F[1+$cw];
						}  else {
							 $self->{cards}->{$ccard}->{IPv4}="";
						}
						$cw+=2; next;
					}
					if($F[$cw] eq "inet6") {
						$self->{cards}->{$ccard}->{IPv6}=$F[1+$cw];
						$cw+=2; next;
					}
					if($F[$cw] eq "broadcast") {
						$self->{cards}->{$ccard}->{BCv4}=$F[1+$cw];
						$cw+=2; next;
					}
					if($F[$cw] eq "netmask") {
						my $a=substr($F[1+$cw],2);
						if($a !~ /\./) {
							my @N=();
							while(length($a)) {
								push @N,hex(substr($a,0,2));
								$a=substr($a,2);
							}
							$a=join(".",@N);
						} 
						$self->{cards}->{$ccard}->{NMv4}=$a;
						$cw+=2; next;
					}
					$cw++;
				}
			} else {
				@F=split(/\s+/,$cline);
				next if ! @F;
				$ccard=$F[0];
				chop($ccard);
			}
		}
		close($fd);
	} elsif ($os eq "HP-UX") {
		my @C=();
		my @C2=();
		open($fd,"/usr/bin/netstat -in|") or confess("Can not read network config details.");
		while($cline=<$fd>) {
			chomp $cline;
			@F=split(/\s+/,$cline);
			if($F[0] =~ /^lan\d+/) {
				push @C2,$F[0];
			}
		}
		close($fd);
		open($fd,"/usr/sbin/lanscan|") or confess("Can not read network config details.");

		while($cline=<$fd>) {
			chomp $cline;
			@F=split(/\s+/,$cline);
			if($F[$#F-2] eq "ETHER") {
				$self->{cards}->{$F[4]}={};
				my $h=substr($F[1],2);
				my $l=substr($h,0,2) . ":" .
					substr($h,2,2) . ":" .
					substr($h,4,2) . ":" .
					substr($h,6,2) . ":" .
					substr($h,8,2) . ":" .
					substr($h,10,2);
				$self->{cards}->{$F[4]}->{HW}=$l;
				$self->{cards}->{$F[4]}->{CI}=$F[2]; # HP-UX only
				if($F[0] =~ /^linkagg/i) {
					$self->{cards}->{$F[4]}->{AGGR}=1;
					$self->{cards}->{$F[4]}->{AGGR_DETECT_RATE_HZ}=0; # not detectable
					$self->{cards}->{$F[4]}->{AGGR_DESC}="HP APA Port Aggregation";
				} else {
					$self->{cards}->{$F[4]}->{AGGR}=0;
					$self->{cards}->{$F[4]}->{AGGR_DETECT_RATE_HZ}=0; # non-zero if detectable
					$self->{cards}->{$F[4]}->{AGGR_DESC}="";
				}

				push @C,$F[4];
				push @ORD,$F[4];
			}
		}
		close($fd);

		#########################################################
		# Lanscan only gives the physical cards, so duplicate	#
		# for logical cards.					#
		#########################################################

		for my $ccard (@C2) {
			my ($phys,$log);
			if($ccard =~ /:/) {
				($phys,$log)=($ccard =~ /^(.*):(.*)/);
				if(exists($self->{cards}->{$phys})) {
					$self->{cards}->{$ccard}={};
					$self->{cards}->{$ccard}->{HW}=$self->{cards}->{$phys}->{HW};
				}
			}
		}
		#########################################################
		# Only ETHER type and HW have been populated up to now.	#
		#########################################################

		my $card;
		for $card (@C2) {
			open($fd,"LD_PRELOAD= /usr/sbin/ifconfig $card|") or confess("Can not run '/usr/sbin/ifconfig $card' - aborting.");
			while($cline=<$fd>) {
				chomp $cline;
				$cline =~ s/^\s+//g;
				next if $cline !~ /^inet/;
				@F=split(/\s+/,$cline);
				$self->{cards}->{$card}->{IPv4}=$F[1];
				$self->{cards}->{$card}->{NMv4}=$F[3];
				$self->{cards}->{$card}->{BCv4}=$F[5];
				$self->{cards}->{$card}->{IPv6}="";
				$self->{cards}->{$card}->{NMv6}="";
				last;
			}
			close($fd);
		}
	} elsif($os eq "FreeBSD") {
		# Freebsd doesn't create virtual alias as separate
		# device instances; instead it does the following:
		# ifconfig re0 add 192.168.122.231
		#
		# ifconfig re0
		# re0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
		#    options=9b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM>
		#    ether 54:52:00:3c:c0:6d
		#    inet 192.168.122.230 netmask 0xffffff00 broadcast 192.168.122.255
		#    inet 192.168.122.231 netmask 0xffffff00 broadcast 192.168.122.255
		#    media: Ethernet autoselect (100baseTX <full-duplex>)
		#    status: active
 
		open($fd,"/sbin/ifconfig|") or confess("ifconfig command could not be run...");
		my $ccard; my $inst=0;
		while($cline=<$fd>) {
			chomp $cline;
			if(substr($cline,0,1) =~ /\s/) {
				next if !defined($ccard);
				$cline =~ s/^\s+//;
				@F=split(/\s+/,$cline);
				if($F[0] eq "inet") {
					if($inst==0) {
						$self->{cards}->{$ccard}->{IPv4}=$F[1];
						if(@F>4) {
							$self->{cards}->{$ccard}->{BCv4}=$F[5];
						} else {
							$self->{cards}->{$ccard}->{BCv4}="0.0.0.0";
						}
						$self->{cards}->{$ccard}->{NMv4}=substr($F[3],2);
					} else {
						$self->{cards}->{"$ccard:$inst"}={};
						$self->{cards}->{"$ccard:$inst"}->{IPv4}=$F[1];
						$self->{cards}->{"$ccard:$inst"}->{BCv4}=$F[5];
						$self->{cards}->{"$ccard:$inst"}->{NMv4}=substr($F[3],2);
						$self->{cards}->{"$ccard:$inst"}->{AGGR}=$self->{cards}->{$ccard}->{AGGR};
						$self->{cards}->{"$ccard:$inst"}->{AGGR_DETECT_RATE_HZ}=$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ};
						$self->{cards}->{"$ccard:$inst"}->{AGGR_DESC}=$self->{cards}->{$ccard}->{AGGR_DESC};
						if(exists($self->{cards}->{$ccard}->{HW})) {
							$self->{cards}->{"$ccard:$inst"}->{HW}=$self->{cards}->{$ccard}->{HW};
						}
					}
					$inst++;
				}
				if($F[0] eq "ether") {
					$self->{cards}->{$ccard}->{HW}=$F[1];
				}
				if($F[0] eq "inet6") {
					my ($j,$j2)=split(/\//,$F[2]);
					$self->{cards}->{$ccard}->{IPv6}=$j;
					$self->{cards}->{$ccard}->{NMv6}=$j2;
					($j,$j2)=split(/:/,$F[3]);
					$self->{cards}->{$ccard}->{SCv6}=$j2;
				}
			} else {
				@F=split(/\s+/,$cline);
				next if ! @F;
				if($F[0] =~ /^lo\d+$/) {
					# ignore loopback cards
					$ccard=undef;
					$inst=0;
					next;
				}
				$ccard=$F[0]; $ccard =~ s/:$//;
				push @ORD,$ccard;
				$inst=0;
				$self->{cards}->{$ccard}={};
				if($ccard =~ /^lagg\d+/) {
					$self->{cards}->{$ccard}->{AGGR}=1;
					$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=0; # non-zero if detectable
					$self->{cards}->{$ccard}->{AGGR_DESC}="Standard FreeBSD Link Aggregation";
				} else {
					$self->{cards}->{$ccard}->{AGGR}=0;
					$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=0; # non-zero if detectable
					$self->{cards}->{$ccard}->{AGGR_DESC}="";
				}
			}
		}
		close($fd);
		# 
	} elsif($os eq "SunOS") {
		# For solaris we use dladm show-dev to get a
		# list of devices and then scan the ifconfig 
		# output for missing ones, and plumb them to get the
		# details ...
		open($fd,"/usr/sbin/dladm show-dev|") or confess("dladm command could not be run...");
		<$fd>;
		my $cline2;
		while($cline2=<$fd>) {
			my @F=split(/\s+/,$cline2);
			if(@F>3 && $F[1] eq "unknown") {
				system("/sbin/ifconfig $F[0] plumb 2>/dev/null >/dev/null");
			}
		}
		close($fd);
		open($fd,"/sbin/ifconfig -a|") or confess("Can not load ifconfig details (/sbin/ifconfig -a)");
		my $ccard;
		while($cline=<$fd>) {
			chomp $cline;
			if(substr($cline,0,1) =~ /\s/) {
				next if !defined($ccard);
				$cline =~ s/^\s+//;
				@F=split(/\s+/,$cline);
				my $cw=0;
				while($cw<@F-1) {
					if($F[$cw] eq "inet") {
						if($F[1+$cw] ne "0.0.0.0") {
							$self->{cards}->{$ccard}->{IPv4}=$F[1+$cw];
						}  else {
							 $self->{cards}->{$ccard}->{IPv4}="";
						}
						$cw+=2; next;
					}
					if($F[$cw] eq "groupname") {
						$self->{cards}->{$ccard}->{AGGR}=1;
						$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=0;
						$self->{cards}->{$ccard}->{AGGR_DESC}="Solaris IPMP group: " . $F[$cw+1];
						$cw+=2; next;
					}
					if($F[$cw] eq "ether") {
						my @F2=split(/:/,$F[1+$cw]);
						$_=sprintf("%02x",hex($_)) foreach(@F2);
						$self->{cards}->{$ccard}->{HW}=join(":",@F2);
						$cw+=2; next;
					}
					if($F[$cw] eq "inet6") {
						$self->{cards}->{$ccard}->{IPv6}=$F[1+$cw];
						$cw+=2; next;
					}
					if($F[$cw] eq "broadcast") {
						$self->{cards}->{$ccard}->{BCv4}=$F[1+$cw];
						$cw+=2; next;
					}
					if($F[$cw] eq "netmask") {
						my $a=$F[1+$cw];
						if($a eq "0") {
							$self->{cards}->{$ccard}->{NMv4}="0.0.0.0";
							$cw+=2; next;
						}
						if($a !~ /\./) {
							my @N=();
							while(length($a)) {
								push @N,hex(substr($a,0,2));
								$a=substr($a,2);
							}
							$a=join(".",@N);
						} 
						$self->{cards}->{$ccard}->{NMv4}=$a;
						$cw+=2; next;
					}
					$cw++;
				}
			} else {
				@F=split(/\s+/,$cline);
				next if ! @F;
				$ccard=$F[0];
				chop($ccard);
				push @ORD,$ccard;
				# cards aggregated via dladm are always aggrN[:N]
				if($ccard =~ /^aggr\d+(:\d+){0,1}$/) {
					$self->{cards}->{$ccard}={};
					$self->{cards}->{$ccard}->{AGGR}=1;
					$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=5;
					$self->{cards}->{$ccard}->{AGGR_DESC}="DLADM Link aggregation";
				} else {
					$self->{cards}->{$ccard}={};
					$self->{cards}->{$ccard}->{AGGR}=0;
					$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=0;
					$self->{cards}->{$ccard}->{AGGR_DESC}="";
				}
			}
		}
		close($fd);
	} else {
		# Linux config here...
		# Rather than rely on names ... use 'Link encap:Ethernet' string
		open($fd,"/sbin/ifconfig -a|") or confess("Can not load ifconfig details (/sbin/ifconfig -a)");
		my $ccard;
		while($cline=<$fd>) {
			chomp $cline;
			if(substr($cline,0,1) =~ /\s/) {
				next if !defined($ccard);
				$cline =~ s/^\s+//;
				@F=split(/\s+/,$cline);
				if($F[0] eq "inet") {
					my ($j,$j2)=split(/:/,$F[1]);
					$self->{cards}->{$ccard}->{IPv4}=$j2;
					($j,$j2)=split(/:/,$F[2]);
					$self->{cards}->{$ccard}->{BCv4}=$j2;
					($j,$j2)=split(/:/,$F[3]);
					$self->{cards}->{$ccard}->{NMv4}=$j2;
				}
				if($F[0] eq "inet6") {
					my ($j,$j2)=split(/\//,$F[2]);
					$self->{cards}->{$ccard}->{IPv6}=$j;
					$self->{cards}->{$ccard}->{NMv6}=$j2;
					($j,$j2)=split(/:/,$F[3]);
					$self->{cards}->{$ccard}->{SCv6}=$j2;
				}
			} else {
				@F=split(/\s+/,$cline);
				next if ! @F;
				if($cline !~ /Link\s+encap:Ethernet/) {
					$ccard=undef;
					next;
				}
				$ccard=$F[0];
				push @ORD,$ccard;
				$self->{cards}->{$ccard}={};
				if($ccard =~ /^bond\d+/) {
					$self->{cards}->{$ccard}->{AGGR}=1;
					$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=0; # non-zero if detectable
					$self->{cards}->{$ccard}->{AGGR_DESC}="Standard Linux Channel Bonding";
				} else {
					$self->{cards}->{$ccard}->{AGGR}=0;
					$self->{cards}->{$ccard}->{AGGR_DETECT_RATE_HZ}=0; # non-zero if detectable
					$self->{cards}->{$ccard}->{AGGR_DESC}="";
				}
				$self->{cards}->{$ccard}->{HW}=$F[4];
			}
		}
		close($fd);
	}
	$self->{order}=[@ORD];
}

sub card_order {
my $self=shift;
	return @{$self->{order}};
}

sub list {
my $self=shift;
my %args=@_;

	if(exists($args{logical})) {
		return (sort(keys(%{$self->{cards}})));
	} else {
		my @c=();
		for my $ckey (sort(keys(%{$self->{cards}}))) {
			push @c,$ckey if $ckey !~ /:/;
		}
		return @c;
	}
}

sub cardref {
my $self=shift;
my $card=shift;

	if(!exists($self->{cards}->{$card})) {
		return undef;
	}
	return $self->{cards}->{$card};
}

# If we think the specified card is part of an external link
# aggregation scheme then return 1, otherwise 0. This will be
# used by the network monitoring daemon to ignore monitoring this
# particlar card/topology.

sub is_aggr {
my $self=shift;
my $card=shift;

	if(!exists($self->{cards}->{$card})) {
		return undef;
	}
	$card =~ s/:.*//; # just interested in phyiscal card
	if(exists($self->{cards}->{$card}->{AGGR})) {
		return $self->{cards}->{$card}->{AGGR};
	}
	return 0;
}

# If an aggregration is returned this will return the 
# failure detection hz - 0 means not detectable.

sub is_aggr_fail_detectable {
my $self=shift;
my $card=shift;

	if(!exists($self->{cards}->{$card})) {
		return undef;
	}
	$card =~ s/:.*//; # just interested in phyiscal card
	if(exists($self->{cards}->{$card}->{AGGR_DETECT_RATE_HZ})) {
		return $self->{cards}->{$card}->{AGGR_DETECT_RATE_HZ};
	}
	return 0;
}

1;

